package com.terrapages.wmsc4j.layer;

import java.awt.image.BufferedImage;
import java.math.BigInteger;
import java.util.Iterator;
import java.util.NoSuchElementException;

import com.terrapages.wmsc4j.bean.BBox;
import com.terrapages.wmsc4j.bean.Pos;
import com.terrapages.wmsc4j.cache.Cache;
import com.terrapages.wmsc4j.layer.resolution.Resolution;
import com.terrapages.wmsc4j.layer.resolution.ResolutionSet;
import com.terrapages.wmsc4j.layer.tile.Tile;
import com.terrapages.wmsc4j.layer.tile.TileFormat;

/**
 * AbstractLayer defines a common set of unique images with in a WMS Cache,
 * The layer is bound to to the spatial box defined by the 'coverage' attribute. 
 * Each image in cache is called a {@link Tile}.
 * Every tile is identical in size (in pixels) but each has a unique bounding box.
 * The first tile at each resolution, from which all other tiles of the same resolution connect,
 * has its south-west corner fixed at the 'origin' (in the same way a cartesian graph works).
 * A layer has a title, a unique name used by WMSC class and DiskCache class to identify it.
 * 
 * @author mvivian
 * @date 15 May 2007
 */
public abstract class AbstractLayer implements Iterable<Tile>{
	private ResolutionSet resolutions;
	private BBox coverage;
	private Pos origin;
	protected String title;
	private Cache cache; 
	private AbstractLayer parent = null;
	private TileFormat format;
	
	/**
	 * @param resolutions
	 * @param format
	 * @param coverage
	 * @param origin
	 * @param title
	 */
	public AbstractLayer(final Cache cache, final ResolutionSet resolutions, final TileFormat format, final BBox coverage, final Pos origin, final String title) 
	{
		this.resolutions = resolutions;
		this.format = format;
		this.coverage = coverage;
		this.origin = origin;
		this.title = title;
		this.cache = cache;
		cache.setUp(this);
	}
	
	protected AbstractLayer(final AbstractLayer layer, ResolutionSet resolutions, BBox coverage)
	{
		parent = layer;
		this.resolutions = resolutions;
		this.coverage = coverage;
		this.format = layer.getFormat();
		this.origin = layer.getOrigin();
		this.title = layer.getTitle();
		this.cache = layer.getCache();
	}
	
	/**
	 * @return the coverage
	 */
	public final BBox getCoverage() {
		return coverage;
	}
	
	/**
	 * @param coverage the coverage to set
	 */
	public final void setCoverage(final BBox coverage) {
		this.coverage = coverage;
	}
	

	/**
	 * @return the format
	 */
	public final TileFormat getFormat() {
		return format;
	}

	/**
	 * @param format the format to set
	 */
	public final void setFormat(final TileFormat format) {
		this.format = format;
	}
	
	/**
	 * @return the origin this is the minX minY of coverage
	 */
	public final Pos getOrigin() {
		return origin;
	}
	
	
	/**
	 * Gets the set of resolutions used by this layer.
	 * @return the resolutions
	 */
	
	public final ResolutionSet getResolutions() {
		return resolutions;
	}

	
	/**
	 * Gets the title of this layer
	 * @return the title
	 */
	public final String getTitle() {
		return title;
	}
	
	protected final Cache getCache() {
		return cache;
	}

	protected final void setCache(Cache cache) {
		this.cache = cache;
	}
	
	/**
	 * When a layer is a subCoverageLayer, it has a parent.
	 * Its parent is a pointer back to the original layer
	 * @return the parent
	 */
	protected final AbstractLayer getParent() {
		return parent;
	}

	/**
	 * When a layer is a subCoverageLayer, it has a parent.
	 * Its parent is a pointer back to the original layer
	 * @param parent the parent to set
	 */
	protected final void setParent(AbstractLayer parent) {
		this.parent = parent;
	}
	
	/**
	 * Returns the number of tiles within this layer.
	 * @return BigInteger(cause there can be a lot). How many tiles are in this layer.
	 */
	public final BigInteger size(){
		BigInteger size = BigInteger.ZERO;

		for(Resolution res: resolutions)
		{
			Tile min = getMinimumTile(res);
			Tile max = getMaximumTile(res);
			
			size = size.add(
					BigInteger.valueOf((max.getXIndex() - min.getXIndex() + 1))
					.multiply(
					BigInteger.valueOf((max.getYIndex() - min.getYIndex() + 1))
					)
			);
		}
		return size;
	}
	
	/**
	 * <p>Gets a layer that makes iteration useful and easy. The new layer is still essentially "this" Layer,
	 * but with a smaller set of resolutions and a smaller coverage. When you iterate a layer, it will give each tile
	 * in the entire coverage at every zoom level. This can be up to Several Billion Tiles!!
	 * By reducing the coverage and zoom levels you can focus your iterations on a specific sub-set of tiles.</p>
	 * 
	 * <p>When/If you do this, be aware of how you match tiles "zoom" with layer "resolution". 
	 * The new layer has less resolutions, so a resolution that used to correspond to a zoom 
	 * (based on index in resolution set) may now correspond to a different zoom. 
	 * Make use of layer.getResolution() and layer.getZoom(). These will match the zooms and resolutions up
	 * properly.</p>
	 * 
	 * @param coverage
	 * @param minZoom
	 * @param maxZoom
	 * @return A smaller sub-set Layer.
	 */
	public abstract AbstractLayer getSubCoverageLayer (BBox extent, int minZoom, int maxZoom);
	

	/**
	 * Gets the tile image from cache and sets the tile image attribute. 
	 * If the image was not in the cache then it is requested from somewhere else which depends on the implementation 
	 */
	public void getTile(Tile tile) {
		BufferedImage old = tile.getImage();
		getCache().get(tile);
		if(tile.getImage()==old)
		{
			notInCacheAction(tile);
		}
	}
	
	/**
	 * When a tile was requested but was not in cache, this method is called
	 * @param tile
	 */
	protected abstract void notInCacheAction(Tile tile);

	
	/* (non-Javadoc)
	 * @see java.lang.Object#hashCode()
	 */
	@Override
	public int hashCode() {
		return title.hashCode();
	}

	/* (non-Javadoc)
	 * @see java.lang.Object#equals(java.lang.Object)
	 */
	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		final AbstractLayer other = (AbstractLayer) obj;
		if (title == null) {
			if (other.title != null)
				return false;
		} else if (!title.equalsIgnoreCase(other.title))
			return false;
		return true;
	}
	
	/**
	 * Gets the most South Western tile in the coverage at the given resolution.
	 * 
	 * @param resolution
	 * @return Tile
	 */
	public Tile getMinimumTile(final Resolution resolution)
	{
		int xIndex=(int)((coverage.getMinLongitude() - origin.getLongitude()) / ( resolution.getValue() * format.getTileSize().getWidth()));
		int yIndex=(int)((coverage.getMinLatitude() - origin.getLatitude()) / ( resolution.getValue() * format.getTileSize().getHeight()));
		int zoom = this.getZoomLevel(resolution);
		return new Tile(this,zoom,xIndex,yIndex);
	}
	
	/**
	 * Gets the most North Eastern tile in the coverage at the given resolution.
	 * 
	 * @param resolution
	 * @return Tile
	 */
	public Tile getMaximumTile(final Resolution resolution)
	{
		int xIndex=(int)((coverage.getMaxLongitude() - origin.getLongitude()) / ( resolution.getValue() * format.getTileSize().getWidth())  - 0.0000001);
		int yIndex=(int)((coverage.getMaxLatitude() - origin.getLatitude()) / ( resolution.getValue() * format.getTileSize().getHeight())  - 0.0000001);
		int zoom = this.getZoomLevel(resolution);
		return new Tile(this,zoom,xIndex,yIndex);
	}
	
	/**
	 * get the zoom level For the given resolution that best matches 
	 * one of the resolutions in the set of resolutions
	 * @param resolution
	 * @return int the zoom level
	 */
	public int getZoomLevel(Resolution resolution)
	{
		if(parent!=null)
		{
			return parent.getZoomLevel(resolution);
		}
		return resolutions.getIndex(resolution);
	}
	
	/**
	 * Gets the resolution of a zoom level.
	 * @param zoom
	 * @return The Resolution of the zoom
	 * @throws IndexOutOfBoundsException if the layer does not support this zoom level.
	 */
	public Resolution getResolution(int zoom)
	{
		if(parent!=null)
		{
			return parent.getResolution(zoom);
		}
		return resolutions.elementAt(zoom);
	}
	
	/**
	 * Gets an iterater that you can use to iterate over every tile in this layer.
	 * Use getSubCoverage if you dont wish to iterate over every single tile in the whole layer.
	 */
	public final Iterator<Tile> iterator()
	{
		return new LayerIterator();
	}
	
	private class LayerIterator implements Iterator<Tile> 
	{
		private transient int xIndex;
		private transient int yIndex;
		private transient int zoom = 0;
		
		private transient Tile min = getMinimumTile(getResolutions().elementAt(zoom));
		private transient Tile max = getMaximumTile(getResolutions().elementAt(zoom));
		
		public LayerIterator() {
			xIndex = min.getXIndex();
			yIndex = min.getYIndex();
			zoom = 0;
		}

		public boolean hasNext() {
			// TODO Auto-generated method stub
			return zoom < getResolutions().size();
		}
		
		public Tile next() {
			if(!hasNext())
			{
				throw new NoSuchElementException();
			}
			
			final Tile retTile = new Tile(AbstractLayer.this, getZoomLevel(getResolutions().elementAt(zoom)), xIndex, yIndex);
			xIndex++;
			if (xIndex>max.getXIndex())
			{
				yIndex++;
				if(yIndex > max.getYIndex())
				{
					zoom++;
					if(hasNext())
					{
						min = getMinimumTile(getResolutions().elementAt(zoom));
						max = getMaximumTile(getResolutions().elementAt(zoom));
					}
					yIndex = min.getYIndex();
				}
				xIndex = min.getXIndex();
			}
			
			
			return retTile;
		}
		
		public void remove() {
			throw new UnsupportedOperationException("You cant remove tiles from a layer");
		}
	}
}
